package eu.ydp.mathplayer.editor.controller;

import eu.ydp.mathplayer.editor.controller.mathml.MathMLTasksProcessor;
import gwt.g2d.client.graphics.Surface;

import java.util.Arrays;
import java.util.List;
import java.util.Vector;

import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.user.client.ui.Panel;
import com.mathplayer.player.MathMLParser;
import com.mathplayer.player.geom.Area;
import com.mathplayer.player.geom.Font;
import com.mathplayer.player.geom.Size;
import com.mathplayer.player.model.LayoutSchemata;
import com.mathplayer.player.model.Token;
import com.mathplayer.player.model.interaction.EmptySpace;
import com.mathplayer.player.model.layoutschematas.MRow;
import com.mathplayer.player.model.tokens.MIdentifier;
import com.mathplayer.player.model.tokens.MNumber;
import com.mathplayer.player.model.tokens.MOperator;
import com.mathplayer.player.utils.BrowserUtils;


public class ModelManager implements ActionSocket {

	private Token model;
	private Panel sanboxOwner;
	private InteractionManager interactionManager;
	private int fontSize = 16;
	private final List<Integer> OPERATORS = Arrays.asList( (new Integer[]{43, 45, 47, 58, 61, 33, 44, 40, 41, 91, 93, 123 ,125 ,60, 62}) );
		
	public ModelManager(String source, Panel sanboxOwner){
		this.sanboxOwner = sanboxOwner;
		interactionManager = new InteractionManager(sanboxOwner, this);

		if (BrowserUtils.getUserAgent().toLowerCase().contains("msie 7")){
			interactionManager.addKeyDownHandler(new KeyDownHandler() {
				public void onKeyDown(KeyDownEvent event) {
					processKeyDown( event.getNativeKeyCode() );
					event.stopPropagation();
					event.preventDefault();
				}
			});
		} else {
			interactionManager.addKeyDownHandler(new KeyDownHandler() {
				public void onKeyDown(KeyDownEvent event) {
					int cc = event.getNativeKeyCode();
					if (KeyDownEvent.isArrow(event.getNativeKeyCode())  ||  event.getNativeKeyCode() == 8  ||  event.getNativeKeyCode() == 46){
						processKeyDown( event.getNativeKeyCode() );
						event.stopPropagation();
						event.preventDefault();
					}
				}
			});
			interactionManager.addKeyPressHandler(new KeyPressHandler() {
				public void onKeyPress(KeyPressEvent event) {
					if (!(event.getCharCode() >= 37  &&  event.getCharCode() <= 40)  &&  event.getCharCode() != 8  &&  event.getCharCode() != 46){
						processKeyPressed(event.getCharCode());
					}
				}
			});
		}
		processSource(source);
	}
	
	public void processSource(String source){
		source = MathMLTasksProcessor.preprocess(source);
		model = MathMLParser.parse(source);
		process();		
	}
	
	private void prepareModel(){
		
		addInputsToEmptyRows(model);
	}
	
	private void addInputsToEmptyRows(Token token){
		if (token instanceof MRow  &&  ((MRow)token).getChildTokensCount() == 0){
			((MRow)token).addChildToken(new EmptySpace());
		} else if (token instanceof LayoutSchemata) {
			for (int i = 0 ; i < ((LayoutSchemata)token).getChildTokensCount() ; i ++){
				addInputsToEmptyRows(((LayoutSchemata)token).getChildTokenAt(i));
			}
		}
	}
	
	private void ensureCursor(){
		
		if (!hasCursor(model)){		
			if (model instanceof MRow){
				((MRow)model).setCursor(0);
			}
		}
	}
	
	private void process(){
		prepareModel();
		ensureCursor();
		render();
	}

	private void render(){
		
		model.setFont(new Font(fontSize, "Times New Roman", false, false));
		
		model.reset();
		Size size = model.measure(interactionManager);
		
		Surface canvas = new Surface((int)size.width, (int)size.height);
		interactionManager.setCanvas(canvas, (int)size.width, (int)size.height);
		
		model.render(canvas, new Area(0, 0, size), interactionManager);

		((LayoutSchemata)model).updateCursor(interactionManager);
		interactionManager.drawCursor();
	}

	private void processKeyDown(int ch){
		boolean doProcess = false;
		
		if ((int)ch >= 37  &&  (int)ch <= 40){
			if ((int)ch == 39)
				moveCursorHorizontally(model, 1);
			if ((int)ch == 37)
				moveCursorHorizontally(model, -1);
			
			((LayoutSchemata)model).updateCursor(interactionManager);
			interactionManager.drawCursor();
		} else if ((int)ch == 8){
			removeAtCursor(model, 0);
			doProcess = true;
		} else if ((int)ch == 46){
			removeAtCursor(model, 1);
			doProcess = true;
		}
		
		if (doProcess){
			process();
		}
	}
	
	/*public void onKeyPressed(int ch) {
		onKeyPressed((char) ch);
	}*/

	@Override
	public void processKeyPressed(char ch) {
		Token newToken = null;
		boolean doProcess = false;
		if (Character.isDigit(ch)){
			newToken = new MNumber(String.valueOf(ch));
		} else  if (Character.isLetter(ch)  && ch != 192){
			newToken = new MIdentifier(String.valueOf(ch));
		} else  if (OPERATORS.contains((Integer)(int)ch)){
			newToken = new MOperator(String.valueOf(ch));
		}   
		
		if (newToken != null){
			insertTokenAtCursor(model, newToken);
			doProcess = true;
		}
		
		if (doProcess){
			process();
		}
	}

	@Override
	public void onClick(int x, int y) {
		setCursorForPoint(model, x, y);
		((LayoutSchemata)model).updateCursor(interactionManager);
		interactionManager.drawCursor();
	}

	@Override
	public void insertToken(InsertTokenType layoutType, Object[] args) {
		createAndInsertTokenAtCursor(model, layoutType, args);
		process();
	}
	
	private boolean createAndInsertTokenAtCursor(Token token, InsertTokenType insertToken, Object[] args){
		if (token instanceof MRow  &&  ((MRow)token).getCursor() != -1){
			Vector<Token> newTokens = TokensFactory.createToken(insertToken, args);
			((MRow)token).addChildToken(newTokens);
			return true;
		} else {
			if (token instanceof LayoutSchemata){
				for (int i = 0 ; i < ((LayoutSchemata)token).getChildTokensCount() ; i ++){
					if (createAndInsertTokenAtCursor( ((LayoutSchemata)token).getChildTokenAt(i), insertToken, args) ){
						return true;
					}
				}
			}
		}
		return false;
	}
	
	private boolean removeAtCursor(Token token, int offset){

		if (token instanceof MRow  &&  ((MRow)token).getCursor() != -1){
			((MRow)token).removeChildTokenAtCursor(offset);
			return true;
		} else {
			if (token instanceof LayoutSchemata){
				for (int i = 0 ; i < ((LayoutSchemata)token).getChildTokensCount() ; i ++){
					boolean removeResult = removeAtCursor( ((LayoutSchemata)token).getChildTokenAt(i) , offset);
					if (removeResult)
						return true;
				}
			}
		}
		return false;
	}
	
	private boolean setCursorForPoint(Token token, int x, int y ){
		if (token.getExactArea().contains(x, y)){
			
			LayoutSchemata layoutToken = (LayoutSchemata)token;
			
			for (int i = 0 ; i < layoutToken.getChildTokensCount() ; i ++){
				if (layoutToken.getChildTokenAt(i) != null  &&  layoutToken.getChildTokenAt(i).getExactArea().contains(x, y)){
					
					if (layoutToken.getChildTokenAt(i) instanceof LayoutSchemata){
						boolean cursorPlaceFound = setCursorForPoint(layoutToken.getChildTokenAt(i), x, y);
						if (cursorPlaceFound)
							return true;
					}
					if (layoutToken instanceof MRow) {
						MRow rowToken = (MRow)token;
						unsetCursor(model);
						if (layoutToken.getChildTokenAt(i).getExactArea().leftHalfContains(x, y)){
							rowToken.setCursor(i);
						} else {
							rowToken.setCursor(i+1);
						}
						return true;
					}
					
				}
			}
		}
		return false;
	}

	private boolean hasCursor(Token t){
		if (t instanceof MRow  &&  ((MRow)t).getCursor() != -1){
			return true;
		} else {
			if (t instanceof LayoutSchemata){
				for (int i = 0 ; i < ((LayoutSchemata)t).getChildTokensCount() ; i ++){
					if (hasCursor( ((LayoutSchemata)t).getChildTokenAt(i) )){
						return true;
					}
				}
			}
		}
		return false;
	}
	
	private void unsetCursor(Token t){
		if (t instanceof MRow  &&  ((MRow)t).getCursor() != -1){
			((MRow)t).setCursor(-1);
		} else {
			if (t instanceof LayoutSchemata){
				for (int i = 0 ; i < ((LayoutSchemata)t).getChildTokensCount() ; i ++){
					unsetCursor( ((LayoutSchemata)t).getChildTokenAt(i) );
				}
			}
		}
	}
	
	private void insertTokenAtCursor(Token currToken, Token newToken){
		if (currToken instanceof MRow  &&  ((MRow)currToken).getCursor() != -1){
			((MRow)currToken).addChildToken(newToken);
			return;
		} else {
			if (currToken instanceof LayoutSchemata){
				for (int i = 0 ; i < ((LayoutSchemata)currToken).getChildTokensCount() ; i ++){
					insertTokenAtCursor( ((LayoutSchemata)currToken).getChildTokenAt(i) , newToken);
				}
			}
		}
		
	}
	
	private enum CursorMoveResult {MOVED, NOT_HERE, BACK_TO_PARENT};
	
	private CursorMoveResult moveCursorHorizontally(Token token, int offset){
		if (token instanceof MRow  &&  ((MRow)token).getCursor() != -1){
			MRow rowToken = ((MRow)token); 
			if (rowToken.getCursor() + offset > rowToken.getChildTokensCount()  ||  rowToken.getCursor() + offset < 0){
				return CursorMoveResult.BACK_TO_PARENT;
			} else if (offset == 1  &&  rowToken.getChildTokenAt(0) instanceof EmptySpace){
				return CursorMoveResult.BACK_TO_PARENT;
			}else {
				int childDueIndex = rowToken.getCursor() + ((offset == -1)?-1:0);
				boolean cursorToChildResult = false;
				if (rowToken.getChildTokenAt(childDueIndex) instanceof LayoutSchemata){
					cursorToChildResult = setCursor(rowToken.getChildTokenAt(childDueIndex), offset == 1);
				} 
				if (!cursorToChildResult){
					rowToken.setCursor(rowToken.getCursor() + offset);
					return CursorMoveResult.MOVED;	
				} else {
					return CursorMoveResult.MOVED;
				}
			}
		} else {
			if (token instanceof LayoutSchemata){
				for (int i = 0 ; i < ((LayoutSchemata)token).getChildTokensCount() ; i ++){
					CursorMoveResult cmr = moveCursorHorizontally( ((LayoutSchemata)token).getChildTokenAt(i) , offset);
					if (cmr == CursorMoveResult.MOVED){
						return CursorMoveResult.MOVED;
					} else if (cmr == CursorMoveResult.BACK_TO_PARENT){
						if (token instanceof MRow){
							MRow rowToken = ((MRow)token);
							if (i + offset > rowToken.getChildTokensCount()){
								rowToken.setCursor(rowToken.getChildTokensCount());
								unsetCursor(((LayoutSchemata)token).getChildTokenAt(i));
								return CursorMoveResult.MOVED;
							} else if (i + offset < 0){
								rowToken.setCursor(0);
								unsetCursor(((LayoutSchemata)token).getChildTokenAt(i));
								return CursorMoveResult.MOVED;
							} else {
								int childDueIndex = i + ((offset == 1)?1:0);
								rowToken.setCursor(childDueIndex);
								unsetCursor(((LayoutSchemata)token).getChildTokenAt(i));
								return CursorMoveResult.MOVED;
							}
						} else {
							return CursorMoveResult.BACK_TO_PARENT;
						}
					}
				}
			}
		}
		return CursorMoveResult.NOT_HERE;
	}
	
	/**
	 * 
	 * @param token
	 * @param atTheBeginning
	 * @return true if cursor was set successfully, false otherwise
	 */
	private boolean setCursor(Token token, boolean atTheBeginning){

		if (token instanceof MRow){
			unsetCursor(model);
			((MRow)token).setCursor(   (atTheBeginning) ? 0 : ((MRow)token).getChildTokensCount()   );
			return true;
		} else {
			if (token instanceof LayoutSchemata){
				for (int i = 0 ; i < ((LayoutSchemata)token).getChildTokensCount() ; i ++){
					if (((LayoutSchemata)token).getChildTokenAt(i) instanceof LayoutSchemata){
						boolean setCursorResult = setCursor(((LayoutSchemata)token).getChildTokenAt(i), atTheBeginning);
						if (setCursorResult)
							return true;
					}
				}
			}
		}
		return false;
	}
	
	public String getMathML(){
		String mml = model.toMathML();
		return MathMLTasksProcessor.postprocess(mml);
	}

	@Override
	public void zoom(boolean in) {
		if (in){
			fontSize += 8;
			if (fontSize > 48)
				fontSize  = 48;
			else
				process();
		} else {
			fontSize -= 8;
			if (fontSize < 8)
				fontSize = 8;
			else
				process();
		}
	}

	@Override
	public void setFocus() {
		interactionManager.setFocus(true);
	}
}
